import random           # FOR RANDOM BEGINNINGS
import Tkinter as tk   # ALL VISUAL EQUIPMENT
#import threading
#import numpy as np
#import copy
#import time
from vec import TwoD, proj #scalar, angle
import itertools # for class instance counters

#debug switch
DEBUG = False

#######  Graphics
WIDTH           = 1000       # OF SCREEN IN PIXELS
HEIGHT          = 800       # OF SCREEN IN PIXELS
BG_COLOR        = 'white'   # background color
FRAMES_PER_SEC  = 40        # SCREEN AND CLIPPING UPDATE RATE
WINDOWED        = True      # MOVABLE PROGRAM
WINDOW_TITLE    = 'Shieldwall'


#### some starting values for agents
GROUND_FRICTION = 0.2 # <- TODO This should go in a "world" class, this is not an attribute of the Agent itself
FORCE           = 1.
ELASTIC         = 0.5            # <- should this be momentum dependend?

N = 50                 # of AGENTS
R = 10                # Radius of Agents

#BRAIN_SAMPLING = 10.

################################################################################

def main():
    # Start the program.
    global agents
    agents = []
    for i in range(N):
        agents.append(Agent(random.random()*WIDTH, random.random()*HEIGHT, radius=R, elasticity=ELASTIC))
    agents[0]=Object(WIDTH/2, HEIGHT/2, radius=R, elasticity=ELASTIC, mass=20, color='green')
    #agents[0]=Object(random.random()*WIDTH, random.random()*HEIGHT, radius=R*2, mass=5., elasticity=ELASTIC)
    build_graph()
    tk.mainloop()

def build_graph():
    # Build GUI environment.
    global graph
    root = tk.Tk()
    if WINDOWED:
        root.resizable(False, False)
        root.title('Shieldwall')
    else:
        root.overrideredirect(True)
    x = (root.winfo_screenwidth() - WIDTH) / 2
    y = (root.winfo_screenheight() - HEIGHT) / 2
    root.geometry('%dx%d+%d+%d' % (WIDTH, HEIGHT, x, y))
    root.bind_all('<Escape>', lambda event: event.widget.quit())
    graph = tk.Canvas(root, width=WIDTH, height=HEIGHT, background=BG_COLOR)
    graph.after(1000 / FRAMES_PER_SEC, update)
    graph.pack()

def update():
    #======Graphics========
    graph.after(1000 / FRAMES_PER_SEC, update)
    graph.delete(tk.ALL)
    
    #======DRAW========
    for agent in agents:
        draw(agent.pos.x, agent.pos.y, agent.radius, agent.color, agent.vel, agent.number)
    graph.update()
    
    #======Collision Detection========
    for agent in agents:
            ##This is brute force, implement relevant preselection!!!
            for other in agents:
                c=collide(agent, other)
                if DEBUG and c: print "collision!"
    
    #======Brains?========
    
    for agent in agents[1:]:
        agent.brain(agents)
    #print agents[1].target_vel
    
    #======Move========
    for agent in agents:
        agent.move()
        
            
def draw(x, y , r, color, vel, text="1"):
    graph.create_oval((x, y, x+2*r, y+2*r), fill=color)
    x1 = x+r
    y1 = y+r
    x2 = x1 + vel.x
    y2 = y1 + vel.y
    graph.create_text(x1, y1, font="Purisa", text=text)
    graph.create_line(x1, y1, x2, y2)
    
    
#========= Helper functions ==========

def bounds(vector, maxi=TwoD(WIDTH,HEIGHT), mini=TwoD(1,1), ): #TODO: Handle drawing edge overlapping Object, maybe in draw()
    """check for bounds, make a donut"""
    if vector.x > maxi.x: vector.x-=maxi.x
    if vector.y > maxi.y: vector.y-=maxi.y
 
    if vector.x < mini.x: vector.x+=maxi.x
    if vector.y < mini.y: vector.y+=maxi.y
    
    return vector


def collide(a, b):
    """collide two Objects, exchange Force and(!) momentum"""
    
    # is it (my)self? Let's not count this as a collision ;-)
    if a==b: return False
    
    d    = (b.pos-a.pos)        # distance vector that points from  A TO B(!)
    dist = d.len()              # scalar length of this
    r_a  = a.radius
    r_b  = b.radius
    
    if dist <= (r_a + r_b):
        d_n = d.norm() # normalized version of the connection vector
        E = (a.elastic + b.elastic) / 2.
        p_a_n = d_n * proj(a.vel*a.mass, d_n)   # projection of the momentum on the distance vector
        p_b_n = d_n * proj(b.vel*b.mass, d_n) 
        f_a_n = d_n * proj(a.force, d_n)        # projection of the force on the distance vector
        f_b_n = d_n * proj(b.force, d_n) 
        
        overlap = (r_a+r_b-dist) * 0.5
        
        a.pos -= (d_n * overlap)
        b.pos += (d_n * overlap)
        
        a.apply_force(((p_b_n - p_a_n)+f_b_n)*E)
        
        b.apply_force(((p_a_n - p_b_n)+f_a_n)*E)                
        return True
    else:#no collision
        return False
    
def in_roi(it, others, roi):
    """return all object's within a roi (radius of interest)"""
    ooi = []
    for other in others:
        if it == other: continue
        d = (it.pos+other.pos)
        if d.len()<=(roi):
            ooi.append(other)
    print ooi
    return ooi


#======= Main class definitions
    
class Object():
    
    newid = itertools.count().next #instance counter
    
    def __init__(self, x, y, mass=1., fric=GROUND_FRICTION, elasticity=1, radius=10., color='red'):
        
        ###### basic motion variables ########
        self.pos   = TwoD(x,y)
        self.vel   = TwoD()
        self.acc   = TwoD()
        self.force = TwoD()
        
        ##### material params #####
        self.color = color
        self.radius = float(radius)
        self.elastic = float(elasticity)
        self.fric = float(fric)
        self.mass = float(mass)
        #self.clipped = False
        self.number = self.__class__.newid()
        
    def apply_force(self, force):
        self.force+=force
        #print self.force
    def _update_acc(self):        
        self.acc = (self.force/self.mass)
        self.force = TwoD(0.,0.)
        #print self.acc
        #print self.force
    def _update_vel(self): 
        self.vel+= self.acc
        self.acc = TwoD(0.,0.)
    def _drag(self):
        speed = self.vel.len()
        if speed <= self.fric: 
            self.vel = TwoD(0.,0.)
            #print "ZERO!!!"
        else: 
            self.vel -= (self.vel.norm()*(self.fric*speed**2.)) 
        
    def _update_pos(self): 
        self.pos+=self.vel
        self.pos = bounds(self.pos)
    
    def move(self):
        #self._tmp=copy(self)# retain 
        self._update_acc()
        self._update_vel()
        self._drag()
        self._update_pos()   
        
    #====Some convenient properties====
        
#    def _get_momentum(self):
#        return self.vel * self.mass
#    def _set_momentum(self, momentum):
#        self.vel = (momentum/self.mass)
#    def _get_speed(self):
#        return self.vel.len()
#    def _set_speed(self, len):
#        return self.vel.set_len(len)
#    
#    p = property(_get_momentum, _set_momentum)
#    speed = property(_get_speed, _set_speed)
         
    

class Agent(Object):
    
    """legal states are: 'roam', 'march'"""
       
    def __init__(self, x, y, mass=1., fric=GROUND_FRICTION, elasticity=1., radius=R, color='red', max_force=FORCE, space=R*2., fov=R*5., fol=R*10.):
        Object.__init__(self, x, y, mass, fric, elasticity, radius, color)
        self.max_force=max_force
        self.space=space
        self.fov=fov
        self.target=None
        self.target_vel=TwoD(0,0)
        self.state = 'roam'
        
    
    
    def brain(self, others):#, local_sampling=0.1):
        """ decides what to do and how to reach a given target"""
        self.visible=self.look()
        self.audibles=self.listen()
        
        self.state=decide()
        
        if state == 'march':
            self.target
    
    
            
    
    def _march(self, others):
        s=0.
        a=0.
        c=0.
        
        sep = (self._separation(others)).norm()
        ali = (self._alignment(others)).norm()
        coh = (self._cohesion(others)).norm()
        
        return ((sep*s) + ((ali*a) + (coh*c)))
        
        
    
    def _separation(self, others):
        target = TwoD(0.,0.)
        for other in others:
            if self == other: continue
            d = (other.pos-self.pos)
            diff = (self.space + other.radius) - d.len()
            if diff > 0.:
                target -= (d.norm()*diff)
                #print target
        return target
                
    
    def _alignment(self, others):
        #_target = self.ve
        target = TwoD(0.,0.)
        for other in others:
            if self == other: continue
            d = (other.pos-self.pos)
            if d.len() <= self.fov:
                target+=other.vel
        return target
    
    def _cohesion(self, others):
        target = TwoD(0.,0.)
        for other in others:
            if self == other: continue
            target+=other.pos
        return target
    
        

class Boss(Agent):
    pass

class HeatMap():
    pass


###############################################################################


# Execute the simulation.
if __name__ == '__main__':
    main()